/*
** Text-based User Interface Library
** Copyright (C) 2008 Eric Day
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public License
** as published by the Free Software Foundation; either version 2
** of the License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to:
** Free Software Foundation, Inc.
** 51 Franklin Street, Fifth Floor,
** Boston, MA  02110-1301, USA
*/

#include "tui_priv.h"

/*
** static functions
*/

/* thread that forwards real stdin to our fake stdin (so poll works) */
static void *_input_thread(void *data);

/* write out a status line to a window, optionally being centered */
static int _set_status(TUI *tui, WINDOW *win, char *status, char center);

/*
** tui functions
*/

/* initialize the tui components with a given application window height */
int tui_init(TUI **new_tui, struct pollfd *pfd, int app_win_height)
{
    TUI *tui;
    int  ret;

    /* treat negative height as disabled */
    if (app_win_height < 0)
        app_win_height = 0;

    tui = calloc(1, sizeof(TUI));
    TUI_RET_CHECK(NULL, tui == NULL, "calloc", errno);

    tui->pfd = pfd;
    tui->input_stdin = -1;
    tui->input_pipe[0] = -1;
    tui->input_pipe[1] = -1;
    tui->app_win_height = app_win_height;

    *new_tui = tui;

    /* We need to dup stdin and do a blocking read on it in a thread since
       Mac OSX doesn't support poll() directly on stdin. In the thread, we
       read from the real stdin and send over a pipe to a simulated (dup2'd)
       stdin that we can poll on. ick...  */

    tui->input_stdin = dup(0);
    TUI_RET_CHECK(tui, tui->input_stdin == -1, "dup", errno);

    ret = pipe(tui->input_pipe);
    TUI_RET_CHECK(tui, ret == -1, "pipe", errno);

    ret = dup2(tui->input_pipe[0], 0);
    TUI_RET_CHECK(tui, ret == -1, "dup2", errno);

    tui->flags |= TUI_FLAGS_INPUT_THREAD;

    ret = pthread_create(&(tui->input_thread_id), NULL, _input_thread, tui);
    if (ret)
        tui->flags &= ~TUI_FLAGS_INPUT_THREAD;
    TUI_RET_CHECK(tui, ret, "pthread_create", ret);

    pfd->fd = 0;
    pfd->events = POLLIN;

    /* initialize the curses interface with five windows (top to bottom):
       scroll, status, input, app status, app */

    /* add one for status window */
    if (app_win_height)
        app_win_height++;

    tui->main_win = initscr();
    TUI_RET_CHECK(tui, tui->main_win == NULL, "initscr", 0);

    TUI_RET_CHECK(tui, app_win_height >= (LINES - 3), "tui_init", EINVAL);

    ret = cbreak();
    TUI_RET_CHECK(tui, ret == ERR, "cbreak", 0);

    ret = noecho();
    TUI_RET_CHECK(tui, ret == ERR, "noecho", 0);

    tui->scroll_win = subwin(tui->main_win, LINES - (2 + app_win_height), COLS,
        0, 0);
    TUI_RET_CHECK(tui, tui->scroll_win == NULL, "subwin scroll", 0);

    ret = scrollok(tui->scroll_win, TRUE);
    TUI_RET_CHECK(tui, ret == ERR, "scrollok", 0);

    tui->status_win = subwin(tui->main_win, 1, COLS,
        LINES - (2 + app_win_height), 0);
    TUI_RET_CHECK(tui, tui->status_win == NULL, "subwin status", 0);

    ret = wattron(tui->status_win, A_BOLD | A_REVERSE);
    TUI_RET_CHECK(tui, ret == ERR, "wattron", 0);

    tui->input_win = subwin(tui->main_win, 1, COLS,
        LINES - (1 + app_win_height), 0);
    TUI_RET_CHECK(tui, tui->input_win == NULL, "subwin input", 0);

    ret = nodelay(tui->input_win, TRUE);
    TUI_RET_CHECK(tui, ret == ERR, "nodelay", 0);

    if (app_win_height)
    {
        tui->app_status_win = subwin(tui->main_win, 1, COLS,
            LINES - app_win_height, 0);
        TUI_RET_CHECK(tui, tui->app_status_win == NULL, "subwin app status", 0);

        ret = wattron(tui->app_status_win, A_BOLD | A_REVERSE);
        TUI_RET_CHECK(tui, ret == ERR, "wattron", 0);

        tui->app_win = subwin(tui->main_win, app_win_height - 1, COLS,
            LINES - (app_win_height - 1), 0);
        TUI_RET_CHECK(tui, tui->app_win == NULL, "subwin app", 0);

        nodelay(tui->app_win, TRUE);
        TUI_RET_CHECK(tui, ret == ERR, "nodelay", 0);
    }

    ret = refresh();
    TUI_RET_CHECK(tui, ret == ERR, "refresh", 0);

    return 0;
}

/* cleanup and destroy the tui components */
void tui_destroy(TUI *tui)
{
    int thread = 0;

    if (tui == NULL)
        return;

    /* clean up the curses interface, erasing the input/app windows so we
       don't leave a garbled command line */
    if (tui->input_win != NULL)
    {
        werase(tui->input_win);
        wrefresh(tui->input_win);
        tui->input_win = NULL;
    }

    if (tui->app_win != NULL)
    {
        werase(tui->app_win);
        wrefresh(tui->app_win);
        tui->app_win = NULL;
    }

    if (tui->main_win != NULL)
    {
        endwin();
        tui->main_win = NULL;
    }

    if (tui->flags & TUI_FLAGS_INPUT_THREAD)
    {
        tui->flags &= ~TUI_FLAGS_INPUT_THREAD;
        thread = 1;
    }

    /* close an fds we opened */
    if (tui->input_stdin != -1)
    {
        /* try to restore stdin, not much we can do if it errors here */
        dup2(tui->input_stdin, 0);
        close(tui->input_stdin);
        tui->input_stdin = -1;
    }

    if (tui->input_pipe[0] != -1)
    {
        close(tui->input_pipe[0]);
        tui->input_pipe[0] = -1;
        close(tui->input_pipe[1]);
        tui->input_pipe[1] = -1;
    }

    if (!thread)
        free(tui);
}

/* set the user prompt */
void tui_set_prompt(TUI *tui, char *prompt)
{
    strncpy(tui->prompt, prompt, TUI_PROMPT_LEN);
    tui->prompt[TUI_PROMPT_LEN - 1] = 0;
    tui->flags |= TUI_FLAGS_INPUT;
}

/* set status message */
int tui_set_status(TUI *tui, char *status, char center)
{
    int ret;

    ret = _set_status(tui, tui->status_win, status, center);
    TUI_RET_CHECK(tui, ret, "_set_status", ret);

    tui->flags |= TUI_FLAGS_STATUS;

    return 0;
}

/* set application status message */
int tui_set_app_status(TUI *tui, char *status, char center)
{
    int ret;

    if (tui->app_status_win == NULL)
        return 0;

    ret = _set_status(tui, tui->app_status_win, status, center);
    TUI_RET_CHECK(tui, ret, "_set_status", ret);

    tui->flags |= TUI_FLAGS_APP_STATUS;

    return 0;
}

/* set application handlers */
void tui_set_app_handlers(TUI *tui, void *app_data,
    int (*app_input)(TUI *tui, void *app_data, int key),
    int (*app_focus)(TUI *tui, void *app_data))
{
    tui->app_data = app_data;
    tui->app_input = app_input;
    tui->app_focus = app_focus;

    /* if focus was on app, switch back to input */
    if (app_input == NULL)
        tui->flags &= ~TUI_FLAGS_APP_FOCUS;
}

/* get application window */
WINDOW *tui_get_app_window(TUI *tui)
{
    return tui->app_win;
}

/* write a message to the scrolling window */
int tui_scroll_write(TUI *tui, char *msg)
{
    int ret;
    int lines;

    if (tui->app_win_height)
        lines = LINES - (4 + tui->app_win_height);
    else
        lines = LINES - (3 + tui->app_win_height);

    ret = scroll(tui->scroll_win);
    TUI_RET_CHECK(tui, ret == ERR, "scroll", 0);

    ret = mvwaddstr(tui->scroll_win, lines, 0, msg);
    TUI_RET_CHECK(tui, ret == ERR, "mvwaddstr", 0);

    tui->flags |= TUI_FLAGS_SCROLL;

    return 0;
}

/* printf a message to the scrolling window */
int tui_scroll_printf(TUI *tui, char *format, ...)
{
    va_list ap;
    char    buf[TUI_INPUT_LEN];

    va_start(ap, format);
    vsnprintf(buf, TUI_INPUT_LEN, format, ap);
    va_end(ap);

    return tui_scroll_write(tui, buf);
}

/* handle user input */
int tui_input(TUI *tui)
{
    int ret;
    int key;

    if (tui->flags & TUI_FLAGS_APP_FOCUS && tui->app_focus != NULL)
        key = wgetch(tui->app_win);
    else
        key = wgetch(tui->input_win);
    TUI_RET_CHECK(tui, key == ERR, "wgetch", 0);

    if (tui->input_state == 0 && key == 0x09)
    {
        /* TAB */
        if (tui->flags & TUI_FLAGS_APP_FOCUS)
            tui->flags &= ~TUI_FLAGS_APP_FOCUS;
        else if (tui->app_input != NULL)
            tui->flags |= TUI_FLAGS_APP_FOCUS;

        return 0;
    }

    if (tui->flags & TUI_FLAGS_APP_FOCUS)
    {
        ret = tui->app_input(tui, tui->app_data, key);
        TUI_RET_CHECK(tui, ret, "tui->app_input", ret);

        return 0;
    }

    tui->flags |= TUI_FLAGS_INPUT;

    switch (tui->input_state)
    {
    case 0:
        if (tui->input_len && (key == 0x08 || key == 0x7F))
        {
            /* DELETE / BACKSPACE */
            if (tui->input_curs > 0)
            {
                if (tui->input_curs != tui->input_len)
                {
                    memmove(tui->input_buf + tui->input_curs - 1,
                            tui->input_buf + tui->input_curs,
                            tui->input_len - tui->input_curs);
                }

                tui->input_curs--;
                tui->input_len--;
            }
        }
        else if (key == 0x0A || key == 0x0D)
        {
            /* NEWLINE / CARRIAGE RETURN */
            tui->flags |= TUI_FLAGS_INPUT_LINE;
        }
        else if (key == 0x15)
        {
            /* CTRL-U */
            tui->input_curs = 0;
            tui->input_len = 0;
        }
        else if (key == 0x1B)
        {
            /* ESC - start escape sequence for special keys */
            tui->input_state = 1;
        }
        else if (tui->input_len < TUI_INPUT_LEN && key >= 0x20 && key <= 0x7E)
        {
            /* PRINTABLE CHAR */
            if (tui->input_curs != tui->input_len)
            {
                memmove(tui->input_buf + tui->input_curs + 1,
                        tui->input_buf + tui->input_curs,
                        tui->input_len - tui->input_curs);
            }

            tui->input_buf[tui->input_curs] = key;
            tui->input_curs++;
            tui->input_len++;
        }

        break;

    case 1:
        if (key == 0x5B)
            tui->input_state = 2; /* ESC+ */
        else
            tui->input_state = 0;

        break;

    case 2:
        if (key == 0x41)
        {
            /* UP ARROW */
        }
        else if (key == 0x42)
        {
            /* DOWN ARROW */
        }
        else if (key == 0x43)
            tui->input_curs++; /* RIGHT ARROW */
        else if (key == 0x44)
            tui->input_curs--; /* LEFT ARROW */

        tui->input_state = 0;

        break;

    default:
        tui->input_state = 0;

        break;
    }

    if (tui->input_curs < 0)
        tui->input_curs = 0;
    else if (tui->input_curs > tui->input_len)
        tui->input_curs = tui->input_len;

    tui->input_buf[tui->input_len] = 0;

    return 0;
}

/* read a line from the input if it is available */
int tui_read(TUI *tui, char *buf, int len)
{
    if (!(tui->flags & TUI_FLAGS_INPUT_LINE))
        return -1;

    tui->flags &= ~TUI_FLAGS_INPUT_LINE;

    if (tui->input_len)
    {
        if (len > tui->input_len)
            len = tui->input_len + 1;

        memcpy(buf, tui->input_buf, len);
        len--;

        tui->input_buf[0] = 0;
        tui->input_len = 0;
        tui->input_curs = 0;
    }
    else
    {
        buf[0] = 0;
        len = 0;
    }

    return len;
}

/* update the tui components */
int tui_update(TUI *tui)
{
    int ret;
    int curs_off;
    int out_len;
    int out_off;
    int width;

    if (tui->flags & TUI_FLAGS_SCROLL)
    {
        tui->flags &= ~TUI_FLAGS_SCROLL;

        ret = wrefresh(tui->scroll_win);
        TUI_RET_CHECK(tui, ret == ERR, "wrefresh", 0);
    }

    if (tui->flags & TUI_FLAGS_STATUS)
    {
        tui->flags &= ~TUI_FLAGS_STATUS;

        ret = wrefresh(tui->status_win);
        TUI_RET_CHECK(tui, ret == ERR, "wrefresh", 0);
    }

    if (tui->flags & TUI_FLAGS_APP_STATUS)
    {
        tui->flags &= ~TUI_FLAGS_APP_STATUS;

        ret = wrefresh(tui->app_status_win);
        TUI_RET_CHECK(tui, ret == ERR, "wrefresh", 0);
    }

    /* calculate input window offsets */
    curs_off = tui->input_curs;
    width = (COLS - (strlen(tui->prompt) + 1));

    if (tui->input_len > width)
    {
        out_len = width;
        out_off = tui->input_curs - width;

        if (tui->input_curs < tui->input_len)
        {
            if ((tui->input_len - tui->input_curs) < (COLS / 2))
                curs_off = (width - (tui->input_len - tui->input_curs));
            else
                curs_off = (width - (COLS / 2));

            out_off = tui->input_curs - curs_off;
            if (out_off < 0)
            {
                out_off = 0;
                curs_off = tui->input_curs;
            }
        }
    }
    else
    {
        out_len = tui->input_len;
        out_off = 0;
    }

    width = 0;

    if (tui->flags & TUI_FLAGS_INPUT)
    {
        tui->flags &= ~TUI_FLAGS_INPUT;
        ret = werase(tui->input_win);
        TUI_RET_CHECK(tui, ret == ERR, "werase", 0);

        ret = mvwaddnstr(tui->input_win, 0, 0, tui->prompt,
            strlen(tui->prompt));
        TUI_RET_CHECK(tui, ret == ERR, "mvwaddnstr", 0);

        /* ignore errors, this function errors when touching the last column */
        mvwaddnstr(tui->input_win, 0, strlen(tui->prompt),
            tui->input_buf + out_off, out_len);

        width = 1;
    }

    if (!(tui->flags & TUI_FLAGS_APP_FOCUS) || tui->app_focus == NULL)
    {
        /* ignore errors, this function errors when touching the last column */
        wmove(tui->input_win, 0, strlen(tui->prompt) + curs_off);

        width = 1;
    }

    if (width)
    {
        ret = wrefresh(tui->input_win);
        TUI_RET_CHECK(tui, ret == ERR, "wrefresh", 0);
    }

    if (tui->flags & TUI_FLAGS_APP_FOCUS && tui->app_focus != NULL)
    {
        ret = tui->app_focus(tui, tui->app_data);
        TUI_RET_CHECK(tui, ret, "tui->app_focus", ret);
    }

    return 0;
}

/*
** static functions
*/

/* thread that forwards real stdin to our fake stdin (so poll works) */
static void *_input_thread(void *data)
{
    TUI  *tui = (TUI *)data;
    int   ret;
    char  c;

    while (1)
    {
        ret = read(tui->input_stdin, &c, 1);

        /* see if we exiting */
        if (!(tui->flags & TUI_FLAGS_INPUT_THREAD))
            break;

        if (ret == -1)
        {
            tui_destroy(tui);
            fprintf(stderr, "read:%d:%s\n", errno, strerror(errno));
            exit(errno);
        }

        ret = write(tui->input_pipe[1], &c, 1);
        if (ret == -1)
        {
            tui_destroy(tui);
            fprintf(stderr, "write:%d:%s\n", errno, strerror(errno));
            exit(errno);
        }
    }

    free(tui);

    return NULL;
}

/* write out a status line to a window, optionally being centered */
static int _set_status(TUI *tui, WINDOW *win, char *status, char center)
{
    int  ret;
    char buf[TUI_MSG_LEN];
    int  len;
    int  win_len;
    
    if (COLS < TUI_MSG_LEN)
        win_len = COLS;  
    else
        win_len = TUI_MSG_LEN;
        
    memset(buf, ' ', win_len);

    if (center)
    {
        len = (win_len - strlen(status)) / 2;
        if (len < 0)
            len = 0;
    }
    else
        len = 0;
        
    len += snprintf(buf + len, win_len - len, "%s", status);
    if (len < win_len)
        buf[len] = ' ';

    ret = werase(win);
    TUI_RET_CHECK(tui, ret == ERR, "werase", 0);
            
    ret = mvwaddnstr(win, 0, 0, buf, win_len);
    /* we ignore this error so we can write the full line. for some
       reason, the function doesn't like writing the last column out */
    /*TGS_RET_CHECK(ret == ERR, "mvwaddnstr", 0);*/

    return 0;
}
